package cn.bincker.mycaptcha.slider;

import cn.bincker.mycaptcha.CacheManager;
import cn.bincker.mycaptcha.basic.CaptchaBackgroundCutter;
import cn.bincker.mycaptcha.basic.impl.AbstractCaptchaFactory;
import cn.bincker.mycaptcha.slider.entity.SliderAnswer;
import cn.bincker.mycaptcha.basic.entity.MyQuestion;
import cn.bincker.mycaptcha.slider.entity.SliderCaptcha;
import cn.bincker.mycaptcha.utils.ImageUtils;
import com.jhlabs.image.GaussianFilter;
import org.springframework.lang.Nullable;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.util.*;

/**
 * 处理验证码
 * 负责提供问题和答案
 */
public class SliderCaptchaFactory extends AbstractCaptchaFactory<SliderCaptcha, SliderAnswer> {
    private final SliderCaptchaProperties properties;
    private final Random random;
    private final Map<Integer, BufferedImage> sliderInsetShadowCache;
    private final Map<Integer, BufferedImage> sliderOutsetShadowCache;
    private final Map<Integer, BufferedImage> imageSlideShadowCache;
    private final CacheManager<SliderCaptcha, SliderAnswer> cacheManager;

    public SliderCaptchaFactory(SliderCaptchaProperties properties, CaptchaBackgroundCutter backgroundCutter, CacheManager<SliderCaptcha, SliderAnswer> cacheManager) {
        super(properties, backgroundCutter);
        this.properties = properties;
        this.cacheManager = cacheManager;
        random = new Random();
        sliderInsetShadowCache = new HashMap<>();
        sliderOutsetShadowCache = new HashMap<>();
        imageSlideShadowCache = new HashMap<>();
    }

    @Override
    public MyQuestion<SliderCaptcha, SliderAnswer> build() {
        var image = buildBackground();
        var captcha = new SliderCaptcha();
        var answer = new SliderAnswer();
        var graphics = (Graphics2D) image.createGraphics();
        var sliderSize = properties.getSliderSize();

//        clip drawImage 抗锯齿没法解决
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        //生成答案和参数
        var xArr = new int[1 + properties.getFakeSliderNumber()];//选取所有的滑块X坐标，包括假滑块
        for (
                int i = 0, j = properties.getWidth() - properties.getMarginLeft() - sliderSize * xArr.length, k = properties.getMarginLeft();//j: 可随机移动的范围, k: 最小可取值
                i < xArr.length;
                i++
        ) {
            var randomMoveX = random.nextInt(j / xArr.length);
            j -= randomMoveX;
            xArr[i] = k + randomMoveX;
            k += randomMoveX + sliderSize;
        }
        var x = xArr[random.nextInt(xArr.length)];
        var y = random.nextInt(properties.getHeight() - properties.getSliderSize());
        answer.setX(x);
        captcha.setY(y);

        //生成扣子
        Boolean[] buckles = getBuckles();
        var topBuckle = buckles[0];
        var bottomBuckle = buckles[1];
        var leftBuckle = buckles[2];
        var rightBuckle = buckles[3];
        var imageSlideArea = generateSlideArea(x, y, topBuckle, bottomBuckle, leftBuckle, rightBuckle);
        var slideArea = imageSlideArea.createTransformedArea(AffineTransform.getTranslateInstance(-x, -y));

        //阴影
        var imageSlideShadow = getImageSlideShadow(slideArea, topBuckle, bottomBuckle, leftBuckle, rightBuckle);

        //绘制滑块
        var slider = drawSliderImage(image, sliderSize, x, y, topBuckle, bottomBuckle, leftBuckle, rightBuckle, imageSlideArea, slideArea);

        //绘制残缺部分
        var maskAlpha = properties.getImageSliderMaskAlpha() + properties.getImageSliderMaskAlphaFloatingRange() * random.nextDouble() * (random.nextBoolean() ? 1 : -1);
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphics.setColor(properties.getImageSliderMaskColor());
        for (var xi : xArr) {
            maskAlpha = properties.getImageSliderMaskAlpha() + properties.getImageSliderMaskAlphaFloatingRange() * random.nextDouble() * (random.nextBoolean() ? 1 : -1);
            graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, (float) maskAlpha));
            imageSlideArea = generateSlideArea(xi, y, topBuckle, bottomBuckle, leftBuckle, rightBuckle);
            graphics.fill(imageSlideArea);
            if(imageSlideShadow != null) {
                graphics.setComposite(AlphaComposite.SrcAtop);
                graphics.drawImage(imageSlideShadow, xi, y, xi + sliderSize, y + sliderSize, 0, 0, sliderSize, sliderSize, null);
            }
        }
        graphics.dispose();

        var question = new MyQuestion<>(captcha, answer);
        output(question, image, slider);

        return question;
    }

    /**
     * 输出滑块图片
     * @param question 问题
     * @param slider 滑块图片
     * @param image 背景图片
     */
    private void output(MyQuestion<SliderCaptcha, SliderAnswer> question, BufferedImage image, BufferedImage slider) {
        var cache = properties.getCache();
        if(cache.getEnabled()){
            cacheManager.put(question, image, slider);
        }else {
            var captcha = question.getCaptcha();
            captcha.setSlider(ImageUtils.convertImageToBase64(slider, "png"));
            captcha.setImage(ImageUtils.convertImageToBase64(image, "png"));
        }
    }

    /**
     * 绘制滑块
     */
    private BufferedImage drawSliderImage(BufferedImage image, Integer sliderSize, int x, int y, Boolean topBuckle, Boolean bottomBuckle, Boolean leftBuckle, Boolean rightBuckle, Area imageSlideArea, Area slideArea) {
        var slider = new BufferedImage(sliderSize, sliderSize, BufferedImage.TYPE_INT_ARGB);
        var sliderGraphics = (Graphics2D) slider.createGraphics();
        //绘制外阴影
        var sliderOutsetShadow = getOutsetSliderShadow(slideArea, topBuckle, bottomBuckle, leftBuckle, rightBuckle);
        sliderGraphics.drawImage(sliderOutsetShadow, 0, 0, sliderSize, sliderSize, 0, 0, sliderSize, sliderSize, null);
        //绘制剪切图片
        drawAntialiasingClipImage(
                sliderGraphics,
                imageSlideArea.createTransformedArea(AffineTransform.getTranslateInstance(-x, -y)),
                image,
                0,
                0,
                sliderSize,
                sliderSize,
                x,
                y,
                x + sliderSize,
                y + sliderSize,
                1
        );
        //绘制内阴影
        if(properties.getSliderInsetShadow().getEnabled()) {
            var sliderInsetShadow = getInsetSliderShadow(slideArea, topBuckle, bottomBuckle, leftBuckle, rightBuckle);
            sliderGraphics.drawImage(sliderInsetShadow, 0, 0, sliderSize, sliderSize, 0, 0, sliderSize, sliderSize, null);
        }
        sliderGraphics.dispose();
        return slider;
    }

    /**
     * 绘制消除锯齿的图片
     * @param graphics 目标画板
     * @param area 绘制区域
     * @param image 绘制图片
     * @param dx 目标x
     * @param dy 目标y
     * @param dw 目标宽度
     * @param dh 目标高度
     * @param sx 源x
     * @param sy 源y
     * @param sw 源宽度
     * @param sh 源高度
     */
    @SuppressWarnings("SameParameterValue")
    private void drawAntialiasingClipImage(Graphics2D graphics, Area area, BufferedImage image, int dx, int dy, Integer dw, Integer dh, int sx, int sy, int sw, int sh, int borderSize) {
        var targetImage = new BufferedImage(dw, dh, BufferedImage.TYPE_INT_ARGB);
        var g = targetImage.createGraphics();
        g.clip(area);
        g.drawImage(image, 0, 0, dw, dh, sx, sy, sw, sh, null);
        g.setComposite(AlphaComposite.DstOut);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(borderSize));
        g.draw(area);
        g.dispose();
        graphics.drawImage(targetImage, dx, dy, dw, dh, 0, 0, dw, dh, null);
    }

    private Boolean[] getBuckles() {
        var buckles = new Boolean[4];
        //至少有一面是空的
        var emptyDirection = random.nextInt(4);
        buckles[emptyDirection] = null;
        var hasInBuckles = false;//是否已经存在一个内扣
        var hasNotBuckles = true;//是否所有方向都没有扣子
        for (int i = 0; i < 4; i++) {
            if(i == emptyDirection) continue;
            if(random.nextBoolean()){//判断是否设空
                buckles[i] = null;
            }else{
                hasNotBuckles = false;
                if(hasInBuckles){//判断是否已经有了一个内扣
                    buckles[i] = true;
                }else{
                    buckles[i] = random.nextBoolean();
                    if(!buckles[i]){
                        hasInBuckles = true;
                    }
                }
            }
        }
        if(hasNotBuckles){//如果所有方向都没有扣子，让没有扣子的那一面随机内外扣
            buckles[emptyDirection] = random.nextBoolean();
        }
        return buckles;
    }

    /**
     * 获取外阴影
     */
    private BufferedImage getOutsetSliderShadow(Area slideArea, Boolean topBuckle, Boolean bottomBuckle, Boolean leftBuckle, Boolean rightBuckle) {
        var cacheKey = getBuckleCacheKey(topBuckle, bottomBuckle, leftBuckle, rightBuckle);
        var cache = sliderOutsetShadowCache.get(cacheKey);
        if(cache != null) return cache;

        var sliderSize = properties.getSliderSize();
        var shadowProperties = properties.getSliderOutsetShadow();

        cache = new BufferedImage(sliderSize, sliderSize, BufferedImage.TYPE_INT_ARGB);
        var graphics = cache.createGraphics();
        var shadow = new BufferedImage(sliderSize, sliderSize, BufferedImage.TYPE_INT_ARGB);
        var shadowGraphics = shadow.createGraphics();
        shadowGraphics.setColor(shadowProperties.getColor());
        shadowGraphics.fill(slideArea);
        if(shadowProperties.getSize() != 0) {
            if (shadowProperties.getSize() < 0) {
                shadowGraphics.setColor(new Color(0, 0, 0, 0));
            }
            shadowGraphics.setStroke(new BasicStroke(Math.abs(shadowProperties.getSize())));
        }
        shadowGraphics.draw(slideArea);
        shadowGraphics.dispose();
        new GaussianFilter(shadowProperties.getBlurRadius()).filter(shadow, shadow);
        graphics.drawImage(shadow, 0, 0, sliderSize, sliderSize, shadowProperties.getX(), shadowProperties.getY(), sliderSize, sliderSize, null);
        sliderOutsetShadowCache.put(cacheKey, cache);
        return cache;
    }

    /**
     * 获取滑块内阴影
     */
    private BufferedImage getInsetSliderShadow(Area area, Boolean topBuckle, Boolean bottomBuckle, Boolean leftBuckle, Boolean rightBuckle) {
        var cacheKey = getBuckleCacheKey(topBuckle, bottomBuckle, leftBuckle, rightBuckle);
        var cache = sliderInsetShadowCache.get(cacheKey);
        if(cache != null) return cache;

        var shadowProperties = properties.getSliderInsetShadow();
        var sliderSize = properties.getSliderSize();

        cache = new BufferedImage(sliderSize, sliderSize, BufferedImage.TYPE_INT_ARGB);
        var graphics = cache.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        var shadow = new BufferedImage(sliderSize, sliderSize, BufferedImage.TYPE_INT_ARGB);
        var shadowGraphics = shadow.createGraphics();
        shadowGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        shadowGraphics.setStroke(new BasicStroke(shadowProperties.getSize() * 2));
        shadowGraphics.setColor(shadowProperties.getColor());
        shadowGraphics.draw(area.createTransformedArea(AffineTransform.getTranslateInstance(shadowProperties.getX(), shadowProperties.getY())));
        shadowGraphics.dispose();
        new GaussianFilter(shadowProperties.getBlurRadius()).filter(shadow, shadow);
        drawAntialiasingClipImage(graphics, area, shadow, 0, 0, sliderSize, sliderSize, 0, 0, sliderSize, sliderSize, shadowProperties.getSize());
        graphics.dispose();
        sliderInsetShadowCache.put(cacheKey, cache);
        return cache;
    }

    /**
     * 获取验证码滑块阴影
     */
    @Nullable
    private BufferedImage getImageSlideShadow(Area area, Boolean topBuckle, Boolean bottomBuckle, Boolean leftBuckle, Boolean rightBuckle) {
        var shadowProperties = properties.getImageSliderShadow();
        if(!shadowProperties.getEnabled()) return null;
        int cacheKey = getBuckleCacheKey(topBuckle, bottomBuckle, leftBuckle, rightBuckle);
        var cache = imageSlideShadowCache.get(cacheKey);
        if(cache != null) return cache;

        var slideSize = properties.getSliderSize();
        var shadow = new BufferedImage(slideSize, slideSize, BufferedImage.TYPE_INT_ARGB);
        cache = new BufferedImage(slideSize, slideSize, BufferedImage.TYPE_INT_ARGB);
        var graphics = shadow.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        graphics.setStroke(new BasicStroke(shadowProperties.getSize() * 2));
        graphics.setColor(shadowProperties.getColor());
        graphics.draw(area.createTransformedArea(AffineTransform.getTranslateInstance(shadowProperties.getX(), shadowProperties.getY())));
        graphics.dispose();
        new GaussianFilter(shadowProperties.getBlurRadius()).filter(shadow, shadow);
        graphics = cache.createGraphics();
        drawAntialiasingClipImage(
                graphics,
                area,
                shadow,
                0,
                0,
                slideSize,
                slideSize,
                0,
                0,
                slideSize,
                slideSize,
                shadowProperties.getSize()
        );
        graphics.dispose();
        imageSlideShadowCache.put(cacheKey, cache);
        return cache;
    }

    private int getBuckleCacheKey(Boolean topBuckle, Boolean bottomBuckle, Boolean leftBuckle, Boolean rightBuckle) {
        var cacheKey = 0;
        if(topBuckle != null) cacheKey |= topBuckle ? 0b10000000 : 0b01000000;
        if(bottomBuckle != null) cacheKey |= bottomBuckle ? 0b00100000 : 0b00010000;
        if(leftBuckle != null) cacheKey |= leftBuckle ? 0b00001000 : 0b00000100;
        if(rightBuckle != null) cacheKey |= rightBuckle ? 0b00000010 : 0b01000001;
        return cacheKey;
    }

    public Area generateSlideArea(int x, int y, Boolean topBuckle, Boolean bottomBuckle, Boolean leftBuckle, Boolean rightBuckle){
        var size = properties.getSliderSize();
        var buckleSize = (int)(size / 3.8);
        var buckleHiddenPercentage = .2f;
        var result = new Area(new Rectangle(
                x + buckleSize,
                y + buckleSize,
                size - buckleSize * 2,
                size - buckleSize * 2
        ));
        double buckleVerticalLeft = x + size * 1.0 / 2 - (buckleSize * 1.0 / 2);
        if(topBuckle != null) {
            if (topBuckle) {
                result.add(new Area(new Arc2D.Double(
                        buckleVerticalLeft,
                        y + buckleSize * buckleHiddenPercentage,
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }else{
                result.subtract(new Area(new Arc2D.Double(
                        buckleVerticalLeft,
                        y + buckleSize * (1 - buckleHiddenPercentage),
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }
        }
        if(bottomBuckle != null) {
            if (bottomBuckle) {
                result.add(new Area(new Arc2D.Double(
                        buckleVerticalLeft,
                        y + size - buckleSize * (1 + buckleHiddenPercentage),
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }else{
                result.subtract(new Area(new Arc2D.Double(
                        buckleVerticalLeft,
                        y + size - buckleSize * (2 - buckleHiddenPercentage),
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }
        }
        double buckleHorizontalTop = y + size * 1.0 / 2 - (buckleSize * 1.0 / 2);
        if(leftBuckle != null) {
            if (leftBuckle) {
                result.add(new Area(new Arc2D.Double(
                        x + buckleSize * buckleHiddenPercentage,
                        buckleHorizontalTop,
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }else{
                result.subtract(new Area(new Arc2D.Double(
                        x + buckleSize * (1 - buckleHiddenPercentage),
                        buckleHorizontalTop,
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }
        }
        if(rightBuckle != null) {
            if (rightBuckle) {
                result.add(new Area(new Arc2D.Double(
                        x + size - buckleSize * (1 + buckleHiddenPercentage),
                        buckleHorizontalTop,
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }else{
                result.subtract(new Area(new Arc2D.Double(
                        x + size - buckleSize * (2 - buckleHiddenPercentage),
                        buckleHorizontalTop,
                        buckleSize,
                        buckleSize,
                        0,
                        360,
                        Arc2D.PIE
                )));
            }
        }
        return result;
    }
}
